package com.baoteng.rbspy.util;

import static java.util.Locale.ENGLISH;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.baoteng.rbspy.annotation.JsoupProperty;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;

public class JsoupUtil {

	private final Logger LOGGER = LoggerFactory.getLogger(JsoupUtil.class);

	private static ConcurrentHashMap<String, List<BeanMethod>> beanMethodMap = new ConcurrentHashMap<String, List<BeanMethod>>();
	public static String strMatch = "(radio|checkbox)";

	private JsoupUtil() {
	}

	private static class JsoupUtilHolder {
		private static final JsoupUtil handler = new JsoupUtil();
	}

	public static JsoupUtil newInstance() {
		return JsoupUtilHolder.handler;
	}

	private boolean compatible(Class<?> getter, Class<?> setter) {
		return setter.isAssignableFrom(getter);
	}

	public static String findValueByRadio(Elements elements) {
		if (elements == null)
			return null;
		for (Element element : elements) {
			if (element.hasAttr("checked")) {
				return element.val();
			}
		}
		return null;
	}

	public static String findValueById(Document doc, String id) {
		if (doc == null)
			return null;
		Element element = doc.getElementById(id);
		return element != null ? element.val() : null;
	}

	public static String findFirstValueByName(Document doc, String name) {
		if (doc == null)
			return null;
		Elements elements = doc.getElementsByAttributeValue("name", name);
		return elements != null && elements.size() > 0 ? elements.get(0).val() : null;
	}

	public static List<String> findValuesByName(Document doc, String name) {
		if (doc == null)
			return null;
		Elements elements = doc.getElementsByAttributeValue("name", name);
		List<String> result = Lists.newArrayList();
		if(elements!=null) {
			elements.forEach(_e -> {
				result.add(_e.val());
			});
		}
		return result.size() > 0 ? result : null;
	}

	/**
	 * 把bean转换成http请求参数
	 * 
	 * @param from
	 * @return
	 */
	@SuppressWarnings("rawtypes")
	public String convert2RequestURLParam(Object from) {
		Preconditions.checkNotNull(from, "convert2RequestURLParam fail.from argument must not be null.");
		Class fromClazz = from.getClass();
		List<BeanMethod> beans = beanMethodMap.get(fromClazz.toString());
		StringBuilder urlParam = new StringBuilder();
		if (beans == null) {
			beans = Lists.newArrayList();
			Field[] getters = ArrayUtils.addAll(fromClazz.getDeclaredFields(),
					fromClazz.getSuperclass().getDeclaredFields());// ReflectUtils.getBeanProperties(fromClazz);Map
																	// names = new HashMap();

			for (Field getter : getters) {
				JsoupProperty property = getter.getAnnotation(JsoupProperty.class);
				if (property != null) {
					if (property.ignore())
						continue;
				}
				try {
					MethodHandles.Lookup lookup = MethodHandles.lookup();

					MethodHandle read = lookup.findVirtual(from.getClass(),
							getBeanMethod(getter.getName(), "get", getter.getType()),
							MethodType.methodType(getter.getType(), new Class[] {}));// lookup.unreflectGetter(getter);//(fromClazz,
																						// getter.getName(),
																						// getter.getType());
					BeanMethod bean = new BeanMethod();
					bean.setReader(read);
					bean.setGetter(getter);

					beans.add(bean);
					String param = invokeParam(from, bean);
					urlParam.append(param != null ? param : "");
				} catch (NoSuchMethodException | IllegalAccessException e) {
					e.printStackTrace();
				}
			}
			if (beans.size() > 0) {
				beanMethodMap.put(fromClazz.toString(), beans);
			}
		} else {
			for (BeanMethod bean : beans) {
				try {
					String param = invokeParam(from, bean);
					urlParam.append(param != null ? param : "");
				} catch (Throwable e) {
					e.printStackTrace();
				}
			}
		}

		if (urlParam != null && urlParam.length() > 0) {
			if (urlParam.substring(urlParam.length() - 1).equals("&")) {
				return urlParam.substring(0, urlParam.length() - 1);
			} else {
				return urlParam.toString();
			}
		} else {
			return null;
		}

	}

	private String invokeParam(Object from, BeanMethod method) {
		StringBuilder param = new StringBuilder();
		try {
			Object read = method.getReader().invoke(from);
			// "",null不转换，
			if (read != null) {
				JsoupProperty property = method.getGetter().getAnnotation(JsoupProperty.class);
				if (property != null) {
					// if(!property.ignore()) {
					// param.append(property.value());
					// method.setDateFormat(property.dateFormat());
					// }else{
					// return null;
					// }
				} else {
					param.append(toUpperForFirstLetter(method.getGetter().getName()));
				}
				param.append("=");
				if (read instanceof String) {
					param.append((String) read);
				} else if (read instanceof java.util.Date) {
					LOGGER.info("date {} --> string {}", method.getReader().toString());
					param.append(StringUtils.isNotBlank(method.getDateFormat())
							? DateFormatUtils.format((Date) read, method.getDateFormat())
							: DateFormatUtils.format((Date) read, "yyyy-MM-dd HH:mm:ss"));
				} else {
					param.append(read.toString());
				}
				param.append("&");
			}
		} catch (Throwable e) {
			e.printStackTrace();
		}
		return param.toString();
	}

	private String toUpperForFirstLetter(String propertyName) {
		return StringUtils.isNotBlank(propertyName)
				? new StringBuilder().append(propertyName.substring(0, 1).toUpperCase())
						.append(propertyName.substring(1)).toString()
				: null;
	}

	/**
	 * html 2 bean
	 * 
	 * @param from
	 * @param to
	 * @return
	 */
	public Object parse(Element doc, Object to) {
		Preconditions.checkNotNull(doc, "parse fail.doc argument must not be null.");
		Preconditions.checkNotNull(to, "parse fail.to argument must not be null.");
		List<BeanMethod> _beans = beanMethodMap.get(to.getClass().toString());
		final List<BeanMethod> beans = _beans != null ? _beans : Lists.newArrayList();
		final MethodHandles.Lookup lookup = MethodHandles.lookup();
		if (beans.isEmpty()) {
			Field[] setters = ArrayUtils.addAll(to.getClass().getDeclaredFields(),
					to.getClass().getSuperclass().getDeclaredFields());// ReflectUtils.getBeanSetters(toClazz);//
			Arrays.stream(setters).forEach(setter -> {
				JsoupProperty property = setter.getAnnotation(JsoupProperty.class);
				if (property == null || property.ignore())
					return;
				// if (property != null && property.ignore())
				// return;

				try {

					MethodHandle write = lookup.findVirtual(to.getClass(),
							getBeanMethod(setter.getName(), "set", setter.getType()),
							MethodType.methodType(void.class, setter.getType()));

					BeanMethod bean = new BeanMethod();
					bean.setWriter(write);
					bean.setSetter(setter);
					bean.setProperty(property);

					beans.add(bean);
				} catch (Throwable e) {
					e.printStackTrace();
				}
			});

			if (!beans.isEmpty()) {
				beanMethodMap.put(to.getClass().toString(), beans);
			}
		}
		for (BeanMethod bean : beans) {
			try {
				if (bean == null || bean.getSetter() == null)
					continue;
				if (bean.getSetter().getType().getClassLoader() != null) {
					Object _bean = bean.getSetter().getType().newInstance();
					parse(doc, _bean);
					bean.getWriter().invoke(to, _bean);
				} else {
					invoke(doc, to, bean);
				}
			} catch (Throwable e) {
				e.printStackTrace();
			}
		}
		try {
			Method method= to.getClass().getDeclaredMethod("parser", Element.class);
			method.setAccessible(true);
			method.invoke(to,doc);
		} catch (Throwable e) {
		}
		return to;
	}

	private String getBeanMethod(String methodName, String prefix, Class<?> propertyType) {
		if ("get".equals(prefix) && propertyType == boolean.class || propertyType == null) {
			return new StringBuilder("is").append(methodName.substring(0, 1).toUpperCase(ENGLISH))
					.append(methodName.substring(1)).toString();
		} else {
			return new StringBuilder(prefix).append(methodName.substring(0, 1).toUpperCase(ENGLISH))
					.append(methodName.substring(1)).toString();
		}
	}

	private void invoke(Element doc, Object to, BeanMethod method) throws Throwable {
		JsoupProperty property = method.getProperty();

		String tag = property != null && StringUtils.isNotBlank(property.tag()) ? property.tag()
				: method.setter.getName();
		String tagValue = property != null && StringUtils.isNotBlank(property.tagValue()) ? property.tagValue() : "";
		String selector = property != null && StringUtils.isNotBlank(property.selector()) ? property.selector()
				: "query";
		String val = property != null && StringUtils.isNotBlank(property.val()) ? property.val() : "val";

		switch (selector) {
		case "id":
			final Element element = doc.getElementById(tag);
			String _v = element != null ? "text".equalsIgnoreCase(val) ? element.text() : element.val() : null;
			setInvoke(method, _v, to);
			break;
		case "attr":
			final Elements elements = doc.getElementsByAttributeValue(tag, tagValue);
			set(elements, to, method, property);
			break;
		default:
			Elements selectElements = doc.select(tag);
			set(selectElements, to, method, property);
		}
		method.getSetter().setAccessible(true);
		LOGGER.info("field attrib ->{}", method.getSetter().get(to));
	}

	private void set(Elements es, Object to, BeanMethod method, JsoupProperty property) throws Throwable {
		boolean single = property != null ? property.single() : false;
		String val = property != null && StringUtils.isNotBlank(property.val()) ? property.val() : "val";
		if (es != null && !es.isEmpty()) {
			final List<String> vList = Lists.newArrayList();

			es.forEach(_element -> {
				switch (_element.tagName()) {
				case "select":
					Elements _es = _element.children();
					if (_es != null && !_es.isEmpty()) {
						_es.forEach(_e -> {
							if (_e.hasAttr("selected")) {
								String ev = _e != null ? "text".equalsIgnoreCase(val) ? _e.text() : _e.val() : null;
								if (StringUtils.isNotBlank(ev))
									vList.add(ev.trim());
							}
						});
					}
					break;
				case "input":
					String eType = _element.attr("type");
					if (StringUtils.isBlank(eType) || !eType.matches(strMatch)
							|| (eType.matches(strMatch) && _element.hasAttr("checked"))) {
						String value = _element != null
								? "text".equalsIgnoreCase(val) ? _element.text() : _element.val()
								: null;
						if (StringUtils.isNotBlank(value))
							vList.add(value.trim());
					}
					break;
				case "textarea":
					String _value = _element.text();
					if (StringUtils.isNotBlank(_value))
						vList.add(_value.trim());
					break;
				case "img":
					String imgSrc = _element.attr("src");
					if (StringUtils.isNotBlank(imgSrc))
						vList.add(imgSrc.trim());
					break;
				default:
					String value = _element != null ? "text".equalsIgnoreCase(val) ? _element.text() : _element.val()
							: null;
					if (StringUtils.isNotBlank(value))
						vList.add(value.trim());
				}
			});
			if (!vList.isEmpty()) {
				if (!single) {
					method.getWriter().invoke(to, vList);
				} else {
					setInvoke(method, vList.get(0), to);
				}
			}
		}
	}

	private void setInvoke(BeanMethod method, String value, Object to) throws Throwable {
		if (StringUtils.isBlank(value))
			return;
		Class<?> writeClazz = method.getSetter().getType();
		if (compatible(writeClazz, String.class)) {
			method.getWriter().invoke(to, value);
		} else if (compatible(writeClazz, Integer.class)) {
			method.getWriter().invoke(to, Integer.valueOf(value));
		} else if (compatible(writeClazz, Double.class)) {
			method.getWriter().invoke(to, Double.valueOf(value));
		} else if (compatible(writeClazz, BigDecimal.class)) {
			method.getWriter().invoke(to, new BigDecimal(value));
		} else if (compatible(writeClazz, Long.class)) {
			method.getWriter().invoke(to, Long.valueOf(value));
		} else if (compatible(writeClazz, Short.class)) {
			method.getWriter().invoke(to, Short.valueOf(value));
		} else if (compatible(writeClazz, Float.class)) {
			method.getWriter().invoke(to, Float.valueOf(value));
		} else if (compatible(writeClazz, Boolean.class)) {
			method.getWriter().invoke(to, "0".equals(value) ? Boolean.TRUE : Boolean.FALSE);
		} else if (compatible(writeClazz, java.util.Date.class)) {
			method.getWriter().invoke(to, DateUtil.fromString(value));
		}
	}
}
